package com.google.ant.extensions.tasks;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Vector;

import com.google.ant.extensions.types.Module;
import com.google.ant.extensions.types.Modules;
import com.google.ant.extensions.types.Resolver;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Ant;
import org.apache.tools.ant.taskdefs.Ant.TargetElement;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.PropertySet;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.util.DateUtils;

public class DrillTask extends AbstractFantTask {

    private Modules modules;

    private Ant ant;
    private String subTarget;
    private boolean verbose;
    private boolean inheritAll;
    private boolean inheritRefs;
    private boolean failOnError = true;

    private String output;

    private Vector properties = new Vector();
    private Vector references = new Vector();
    private Vector propertySets = new Vector();

    /** the targets to call on the new project */
    private Vector/*<TargetElement>*/ targets = new Vector();

    /**
     * Pass output sent to System.out to the new project.
     *
     * @param output a line of output
     * @since Ant 1.6.2
     */
    public void handleOutput(String o) {
        if (ant != null) {
            ant.handleOutput(o);
        } else {
            super.handleOutput(o);
        }
    }

    /**
     * Process input into the ant task
     *
     * @param buffer the buffer into which data is to be read.
     * @param offset the offset into the buffer at which data is stored.
     * @param length the amount of data to read
     *
     * @return the number of bytes read
     *
     * @exception IOException if the data cannot be read
     *
     * @see Task#handleInput(byte[], int, int)
     *
     * @since Ant 1.6.2
     */
    public int handleInput(byte[] buffer, int offset, int length)
        throws IOException {
        if (ant != null) {
            return ant.handleInput(buffer, offset, length);
        } else {
            return super.handleInput(buffer, offset, length);
        }
    }

    /**
     * Pass output sent to System.out to the new project.
     *
     * @param output The output to log. Should not be <code>null</code>.
     *
     * @since Ant 1.6.2
     */
    public void handleFlush(String o) {
        if (ant != null) {
            ant.handleFlush(o);
        } else {
            super.handleFlush(o);
        }
    }

    /**
     * Pass output sent to System.err to the new project.
     *
     * @param output The error output to log. Should not be <code>null</code>.
     *
     * @since Ant 1.6.2
     */
    public void handleErrorOutput(String o) {
        if (ant != null) {
            ant.handleErrorOutput(o);
        } else {
            super.handleErrorOutput(o);
        }
    }

    /**
     * Pass output sent to System.err to the new project.
     *
     * @param output The error output to log. Should not be <code>null</code>.
     *
     * @since Ant 1.6.2
     */
    public void handleErrorFlush(String o) {
        if (ant != null) {
            ant.handleErrorFlush(o);
        } else {
            super.handleErrorFlush(o);
        }
    }

    private void printStarter(Collection<Module> ms) {
        handleOutput("Scanning for projects...");
        handleOutput("Build order:");
        for (Module m : ms) {
            handleOutput("  " + m.getNiceName());
        }
        handleOutput("\n ----------------------------------------------------------------------------\n");
    }

    private String getDots(int length) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 60 - length; i++) {
            sb.append(".");
        }
        return sb.toString();
    }

    private void printSummary(Collection<Module>builds) {
        handleOutput("  Summary:");
        int max = 65;
        StringBuffer sb = new StringBuffer();
        for (Module m : builds) {
            String line = m.getNiceName();
            String elapseString = DateUtils.formatElapsedTime(m.getElapsed());
            String dots = getDots(line.length() + elapseString.length());

            sb.append(line + " " + dots + " [" + elapseString + "]");
            sb.append("\n");
        }

        String hr = getHR(max);
        handleOutput(hr);
        handleOutput(sb.toString());
        handleOutput(hr);
    }

    private String getHR(int size) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < size; i++) {
            sb.append("-");
        }
        return sb.toString();
    }

    public void execute() {
        if (modules == null) {
            throw new BuildException("No modules specified");
        }

        Collection<Module> builds = new ArrayList<Module>();
        new Resolver().resolve2(modules.listAll(), builds, 0);
        
        //System.out.println(builds);        
        printStarter(builds);

        if (builds.size() < 1) {
            log("No sub-builds to iterate on", Project.MSG_WARN);
            return;
        }
        BuildException buildException = null;
        for (Module module : builds) {
            handleOutput("---------------------------------------------------------------");
            handleOutput("Module: " + module.getNiceName());
            handleOutput("Base: " + module.getBasedir());
            handleOutput("Target: [" + subTarget + "]");
            handleOutput("---------------------------------------------------------------");

            File file = null;
            File directory = null;
            Throwable thrownException = null;
            try {
                directory = new File(module.getBasedir());
                file = new File(module.getBasedir(), module.getBuildfile());
                long start = System.currentTimeMillis();
                execute(file, directory);
                module.setElapsed(System.currentTimeMillis() - start);
                if (verbose) {
                    log("Leaving directory: " + directory + "\n", Project.MSG_INFO);
                }
            } catch (RuntimeException ex) {
                if (!(getProject().isKeepGoingMode())) {
                    if (verbose) {
                        log("Leaving directory: " + directory + "\n", Project.MSG_INFO);
                    }
                    throw ex;
                }
                thrownException = ex;
            } catch (Throwable ex) {
                if (!(getProject().isKeepGoingMode())) {
                    if (verbose) {
                        log("Leaving directory: " + directory + "\n", Project.MSG_INFO);
                    }
                    throw new BuildException(ex);
                }
                thrownException = ex;
            }
            if (thrownException != null) {
                if (thrownException instanceof BuildException) {
                    log("File '" + file
                        + "' failed with message '"
                        + thrownException.getMessage() + "'.", Project.MSG_ERR);
                    // only the first build exception is reported
                    if (buildException == null) {
                        buildException = (BuildException) thrownException;
                    }
                } else {
                    log("Target '" + file
                        + "' failed with message '"
                        + thrownException.getMessage() + "'.", Project.MSG_ERR);
                    thrownException.printStackTrace(System.err);
                    if (buildException == null) {
                        buildException =
                            new BuildException(thrownException);
                    }
                }
                if (verbose) {
                    log("Leaving directory: " + directory + "\n", Project.MSG_INFO);
                }
            }
        }
        //check if one of the builds failed in keep going mode
        if (buildException != null) {
            throw buildException;
        }

        printSummary(builds);
    }

    /**
     * Runs the given target on the provided build file.
     *
     * @param  file the build file to execute
     * @param  directory the directory of the current iteration
     * @throws BuildException is the file cannot be found, read, is
     *         a directory, or the target called failed, but only if
     *         <code>failOnError</code> is <code>true</code>. Otherwise,
     *         a warning log message is simply output.
     */
    private void execute(File file, File directory)
        throws BuildException {
        if (!file.exists() || file.isDirectory() || !file.canRead()) {
            String msg = "Invalid file: " + file;
            if (failOnError) {
                throw new BuildException(msg);
            }
            log(msg, Project.MSG_WARN);
            return;
        }
        ant = createAntTask(directory);
        String antfilename = file.getAbsolutePath();
        ant.setAntfile(antfilename);
        for (int i = 0; i < targets.size(); i++) {
            TargetElement targetElement = (TargetElement) targets.get(i);
            ant.addConfiguredTarget(targetElement);
        }

        try {
            ant.execute();
        } catch (BuildException e) {
            if (failOnError) {
                throw e;
            }
            log("Failure for target '" + subTarget
                + "' of: " +  antfilename + "\n"
                + e.getMessage(), Project.MSG_WARN);
        } catch (Throwable e) {
            if (failOnError) {
                throw new BuildException(e);
            }
            log("Failure for target '" + subTarget
                + "' of: " + antfilename + "\n"
                + e.toString(),
                Project.MSG_WARN);
        } finally {
            ant = null;
        }
    }


    public void setFailonerror(boolean f) {
        this.failOnError = f;
    }

    /**
     * The target to call on the different sub-builds. Set to "" to execute
     * the default target.
     * @param target the target
     * <p>
     */
    //     REVISIT: Defaults to the target name that contains this task if not specified.
    public void setTarget(String target) {
        this.subTarget = target;
    }

    /**
     * Add a target to this Ant invocation.
     * @param t the <code>TargetElement</code> to add.
     * @since Ant 1.7
     */
    @SuppressWarnings("unchecked")
    public void addConfiguredTarget(TargetElement t) {
        String name = t.getName();
        if ("".equals(name)) {
            throw new BuildException("target name must not be empty");
        }
        targets.add(t);
    }

    /**
     * Enable/ disable verbose log messages showing when each sub-build path is entered/ exited.
     * The default value is "false".
     * @param on true to enable verbose mode, false otherwise (default).
     */
    public void setVerbose(boolean on) {
        this.verbose = on;
    }

    /**
     * Corresponds to <code>&lt;ant&gt;</code>'s
     * <code>output</code> attribute.
     *
     * @param  s the filename to write the output to.
     */
    public void setOutput(String s) {
        this.output = s;
    }

    /**
     * Corresponds to <code>&lt;ant&gt;</code>'s
     * <code>inheritall</code> attribute.
     *
     * @param  b the new value for this boolean flag.
     */
    public void setInheritall(boolean b) {
        this.inheritAll = b;
    }

    /**
     * Corresponds to <code>&lt;ant&gt;</code>'s
     * <code>inheritrefs</code> attribute.
     *
     * @param  b the new value for this boolean flag.
     */
    public void setInheritrefs(boolean b) {
        this.inheritRefs = b;
    }

    /**
     * Corresponds to <code>&lt;ant&gt;</code>'s
     * nested <code>&lt;property&gt;</code> element.
     *
     * @param  p the property to pass on explicitly to the sub-build.
     */
    @SuppressWarnings("unchecked")
    public void addProperty(Property p) {
        properties.addElement(p);
    }

    /**
     * Corresponds to <code>&lt;ant&gt;</code>'s
     * nested <code>&lt;reference&gt;</code> element.
     *
     * @param  r the reference to pass on explicitly to the sub-build.
     */
    @SuppressWarnings("unchecked")
    public void addReference(Ant.Reference r) {
        references.addElement(r);
    }

    /**
     * Corresponds to <code>&lt;ant&gt;</code>'s
     * nested <code>&lt;propertyset&gt;</code> element.
     * @param ps the propertset
     */
    @SuppressWarnings("unchecked")
    public void addPropertyset(PropertySet ps) {
        propertySets.addElement(ps);
    }

    private Ant createAntTask(File directory) {
        Ant antTask = new Ant(this);
        antTask.init();
        if (subTarget != null && subTarget.length() > 0) {
            antTask.setTarget(subTarget);
        }


        if (output != null) {
            antTask.setOutput(output);
        }

        if (directory != null) {
            antTask.setDir(directory);
        }

        antTask.setInheritAll(inheritAll);
        for (Enumeration i = properties.elements(); i.hasMoreElements();) {
            copyProperty(antTask.createProperty(), (Property) i.nextElement());
        }

        for (Enumeration i = propertySets.elements(); i.hasMoreElements();) {
            antTask.addPropertyset((PropertySet) i.nextElement());
        }

        antTask.setInheritRefs(inheritRefs);
        for (Enumeration i = references.elements(); i.hasMoreElements();) {
            antTask.addReference((Ant.Reference) i.nextElement());
        }

        return antTask;
    }

    /**
     * Assigns an Ant property to another.
     *
     * @param  to the destination property whose content is modified.
     * @param  from the source property whose content is copied.
     */
    private static void copyProperty(Property to, Property from) {
        to.setName(from.getName());

        if (from.getValue() != null) {
            to.setValue(from.getValue());
        }
        if (from.getFile() != null) {
            to.setFile(from.getFile());
        }
        if (from.getResource() != null) {
            to.setResource(from.getResource());
        }
        if (from.getPrefix() != null) {
            to.setPrefix(from.getPrefix());
        }
        if (from.getRefid() != null) {
            to.setRefid(from.getRefid());
        }
        if (from.getEnvironment() != null) {
            to.setEnvironment(from.getEnvironment());
        }
        if (from.getClasspath() != null) {
            to.setClasspath(from.getClasspath());
        }
    }

    public void setModulesRef(Reference r) {
        Object o = r.getReferencedObject(getProject());
        if (!(o instanceof Modules)) {
            String msg = r.getRefId() + " doesn\'t denote a modules";
            throw new BuildException(msg);
        }
        modules = (Modules)o;
    }
}
